/* Wp-spp by Hui Chen
 
   Wp-spp stands for "Wordpress Search Plus Plus". It is a C++ project to create 
   an effective search engine for Wordpress. This CGI program connects to MySQL 
   database via interface routines provided by MySQL++ and uses boost library to 
   handle keyword searching and highlighting.

   Compiled CGI code of wp-spp should be at least an order of magnitude faster 
   than its PHP competitor when using complicated searching logic.
 
   Features include (not a complete list):
 
   * search on both posts and comments
   * highlight search results
   * compatible with UTF8 encoding
   * controllable context length before/after highlighted keywords
   * suppress HTML tags in search results
   * display posts and comments in a tree structure
   * paging
   * support various search strategies for multiple keywords 

   This project is published under GNU Lesser General Public License.

   Project homepage:
   http://code.google.com/p/wp-spp/

   Author homepage:
   http://huichen.org

   A demo of wp-spp can be found at (Chinese)
   http://limiao.net/search.it
*/

#include <iostream>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <mysql++.h>
#include <regex.hpp>
#include <connection.h>
#include <date_time/posix_time/posix_time.hpp>
#include <CgiDefs.h>
#include <Cgicc.h>
#include <HTTPHTMLHeader.h>
#include <HTMLClasses.h>

using namespace std;
using namespace cgicc;
using namespace boost;
using namespace mysqlpp;

const string mysql_db = ""; //mysql database for Wordpress
const string mysql_hostname = "localhost"; // mysql database hostname
const string mysql_username = ""; //mysql username
const string mysql_password = ""; //mysql password
const string mysql_posts_table = "wp_posts"; //mysql table for posts
const string mysql_comments_table = "wp_comments"; //mysql table for comments

const int max_post_char = 600; // maximum characters displayed in a post
const int post_before_hl_char = 200; // maximum characters displayed before highlighted keyword
const int post_after_hl_char = 400; // maximum characters displayed before highlighted keyword
const int max_comment_char = 400; // maximum characters displayed in a comment
const int comment_before_hl_char = 200; // maximum characters displayed before highlighted keyword
const int comment_after_hl_char = 200; // maximum characters displayed before highlighted keyword
const int max_searchkeyword_size = 60; // maximum allowed keyword length
const int max_comments_per_post = 50; // maximum comments displayed under a post

// customize how following strings are displayed in your page
const string str_page_title = "Wordpress Search Engine"; //page title
const string str_page_heading = "Search";	// actual heading is str_page_heading + " 'keyword'"
const string str_search_button_name = "Search"; // search button name
const string str_case_sensitive = "case sensitive";
const string str_ignore_comments = "ignore comments";
const string str_use_or_logic = "use 'or' logic for keywords";
const string str_posts_per_page = "posts per page";
const string str_previous_page = "previous page";
const string str_next_page = "next page";
const string str_seconds = "seconds";
const string str_input_search_keyword = "Input keyword";
const string str_keyword_too_long = "Keyword is too long!";
const string str_cannot_connect_to_db = "Can't connect to database!";
const string str_illegal_page_number = "Illegal page number!";
const string str_more_comments_in_results = "more comments in results";
const string str_no_result_found = "No record is found!";
const string str_unknown_error = "Unkown error!";
const string site_counter = ""; // put your site counter script here

/* a Chinese example of str_* consts
const string str_page_title = "全站搜索引擎";
const string str_page_heading = "搜索";	// actual heading is str_page_heading + " 'keyword'"
const string str_search_button_name = "搜索";
const string str_case_sensitive = "区分大小写";
const string str_ignore_comments = "忽略评论";
const string str_use_or_logic = "多关键词使用'或'逻辑";
const string str_posts_per_page = "每页文章数";
const string str_previous_page = "上页";
const string str_next_page = "下页";
const string str_seconds = "秒";
const string str_input_search_keyword = "请输入关键词进行搜索";
const string str_keyword_too_long = "关键词太长!";
const string str_cannot_connect_to_db = "无法连接到数据库!";
const string str_illegal_page_number = "非法页码!";
const string str_more_comments_in_results = "更多符合条件的评论";
const string str_no_result_found = "很遗憾，没有您要的结果!";
const string str_unknown_error = "未知错误!";
const string site_counter = "";
*/

int
main (int argc, char **argv)
{
  posix_time::ptime starttime, endtime;
  posix_time::time_duration duration;
  starttime = posix_time::microsec_clock::local_time ();
  int posts_per_page = 10; //default posts per page
  ostream_iterator < char >out (cout);
  try
  {
    // display html header, title etc
    cout << HTTPHTMLHeader ();
    cout << HTMLDoctype (HTMLDoctype::eStrict) << endl;
    cout << "<html>" << head () << "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />" << endl;
    cout << title (str_page_title);
    cout << endl << "<link rel=\"stylesheet\" href=\"wpspp.css\" type=\"text/css\" media=\"screen\" /><script>function sf(){document.go.s.focus()};</script>" << head () << endl;
    cout << body ().set ("bgcolor", "#FFFFFF").set ("text", "#000000").set ("link", "#0000ff").set ("vlink", "#000080").set ("onload", "sf();") << endl;

    Cgicc formData;
    form_iterator s = formData.getElement ("s"); // search keywords
    string searchkeyword; // keyword trim space
    string searchkeyword_regex; 
    form_iterator p = formData.getElement ("p"); // page number
    int page;
    form_iterator cs = formData.getElement ("cs"); //flag of case sensitivity
    bool case_sensitive;
    form_iterator ic = formData.getElement ("ic"); // flag of ignore comments
    bool ignore_comments;
    form_iterator ppp = formData.getElement ("ppp"); //posts per page
    int ppp_n;
    form_iterator uol = formData.getElement ("uol"); // flag of using 'or' logic
    bool use_or_logic;

    string searchform;

    // default search form
    string defaultsearchform = "<div class=\"searchform\"><form name=\"go\" method=\"post\" action=\"wpspp.cgi\"><input type=\"text\" name=\"s\" value=\"\"/> <input name=\"sub\" value=\"" + str_search_button_name + "\" type=\"submit\"> &nbsp; <input type=\"checkbox\" name=\"cs\"> " + str_case_sensitive + " &nbsp; <input type=\"checkbox\" name=\"ic\"> " + str_ignore_comments + " &nbsp; <input type=\"checkbox\" name=\"uol\"> " + str_use_or_logic + " &nbsp; <select name=\"ppp\" onchange=\"document.go.submit();\"><option value=\"10\" selected=\"yes\">10</option><option value=\"20\">20</option><option value=\"50\">50</option><option value=\"100\">100</option></select> " + str_posts_per_page + "</form></div>";

    // assign posts_per_page
    if (ppp != formData.getElements ().end ())
      {
	ppp_n = atoi (ppp->getValue ().c_str ());
	switch (ppp_n)
	  {
	  case 20:
	    posts_per_page = 20;
	    break;
	  case 50:
	    posts_per_page = 50;
	    break;
	  case 100:
	    posts_per_page = 100;
	    break;
	  default:
	    posts_per_page = 10;
	  }
      }
    else
      posts_per_page = 10;

    //assign case_sensitive
    if (cs != formData.getElements ().end ())
      {
	case_sensitive = true;
      }
    else
      {
	case_sensitive = false;
      }

    //assign ignore_comments
    if (ic != formData.getElements ().end ())
      {
	ignore_comments = true;
      }
    else
      {
	ignore_comments = false;
      }

    //assign use_or_logic
    if (uol != formData.getElements ().end ())
      {
	use_or_logic = true;
      }
    else
      {
	use_or_logic = false;
      }

    //assign searchkeyword
    if (s != formData.getElements ().end ())
      {
	searchkeyword = s->getValue ();
      }
    else
      {
	cout << "<div class=\"heading\">" << h1 (str_input_search_keyword) << "</div>" << endl << defaultsearchform << endl;
	cout << endl << site_counter << endl << "</body></html>" << endl;
	return 0;
      }

    //trim searchkeyword
    int itrim_b = 0;
    int itrim_e = searchkeyword.size () - 1;

    while (itrim_b < searchkeyword.size () && searchkeyword[itrim_b] == ' ')
      itrim_b++;
    while (itrim_e >= 0 && searchkeyword[itrim_e] == ' ')
      itrim_e--;

    if (itrim_b <= itrim_e)
      searchkeyword = string (searchkeyword.begin () + itrim_b, searchkeyword.begin () + itrim_e + 1);
    else
      {
	cout << "<div class=\"heading\">" << h1 (str_input_search_keyword) << "</div>" << endl << defaultsearchform << endl;
	cout << endl << site_counter << endl << "</body></html>" << endl;
	return 0;
      }
      //end trimming searchkeyword
      
    if (use_or_logic) // replace " " with "|" in searchkeyword if use_or_logic, 
                        //put result string in searchkeyword_regex
      searchkeyword_regex = regex_replace (searchkeyword, regex ("( +)"), "(\\|)", match_default | format_all);
    else
      searchkeyword_regex = regex_replace (searchkeyword, regex ("( +)"), "( )", match_default | format_all);

      searchkeyword_regex = regex_replace (searchkeyword, regex ("( +)|(\\.)|(\\[)|(\\])|(\\{)|(\\})|(\\^)|(\\$)|(\\*)|(\\+)|(\\?)|(\\\\)"), 
      "(?1 )(?2\\\\.)(?3\\\\[)(?4\\\\])(?5\\\\{)(?6\\\\})(?7\\\\^)(?8\\\\$)(?9\\\\*)(?10\\\\+)(?11\\\\?)(?12\\\\\\\\)", 
      match_default | format_all); // replace regex special characters
      searchkeyword_regex = regex_replace (searchkeyword_regex, regex ("\\("), "\\\\(", match_default);
      searchkeyword_regex = regex_replace (searchkeyword_regex, regex ("\\)"), "\\\\)", match_default);

    // return if keyword is longer than max_searchkeyword_size 
    if (searchkeyword_regex.size () > max_searchkeyword_size)
      {
	cout << "<div class=\"heading\">" << h1 (str_keyword_too_long) << "</div>" << endl << defaultsearchform << endl;;
	cout << endl << site_counter << endl << "</body></html>" << endl;
	return 0;
      }
    else if (searchkeyword_regex.size () == 0)
      {
	cout << "<div class=\"heading\">" << h1 (str_input_search_keyword) << "</div>" << endl << searchform << endl;
	cout << endl << site_counter << endl << "</body></html>" << endl;
	return 0;
      }

    // searchform
    searchform = "<div class=\"searchform\"><form name=\"go\" method=\"post\" action=\"wpspp.cgi\"><input type=\"text\" name=\"s\" value=\"" + searchkeyword + "\"/> <input name=\"submit\" value=\"" + str_search_button_name + "\" type=\"submit\"> &nbsp; <input type=\"checkbox\" name=\"cs\" " + (case_sensitive ? string ("checked") : string ("")) + "> " + str_case_sensitive + " &nbsp; <input type=\"checkbox\" name=\"ic\" " + (ignore_comments ? string ("checked") : string ("")) + "> " + str_ignore_comments + " &nbsp; <input type=\"checkbox\" name=\"uol\" " + (use_or_logic ? string ("checked") : string ("")) + "> " + str_use_or_logic + " &nbsp; <select name=\"ppp\" onchange=\"javascript:document.go.submit();\"><option value=\"10\" " + string ((posts_per_page == 10 ? "selected=\"yes\"" : "")) + ">10</option><option value=\"20\" " + string ((posts_per_page == 20 ? "selected=\"yes\"" : "")) + ">20</option><option value=\"50\" " + string ((posts_per_page == 50 ? "selected=\"yes\"" : "")) + ">50</option><option value=\"100\" " + string ((posts_per_page == 100 ? "selected=\"yes\"" : "")) + ">100</option></select> " + str_posts_per_page + "</form></div>";

    // assign page number
    if (p == formData.getElements ().end ())
      {
	page = 0;
      }
    else
      page = atoi (string (p->getValue ()).c_str ());

    //connect to db
    Connection con (false);
    con.set_option (Connection::opt_set_charset_name, "utf8");
    if (!con.connect (mysql_db.c_str (), mysql_hostname.c_str (), mysql_username.c_str (), mysql_password.c_str ()))
      {
	cout << "<div class=\"heading\">" << h1 (str_cannot_connect_to_db) << "</div>" << endl << searchform << endl;;
	cout << endl << site_counter << endl << "</body></html>" << endl;
	return 1;
      }

    Result res, res1;
    Query query = con.query ();
    query.use ("SET NAMES 'utf8'"); // use UTF8 in connection
    res = query.store (("select * from " + mysql_posts_table + " where (post_status='publish' or post_status='static') order by post_date DESC ").c_str ()); // select wordpress posts table

    // return if page number is illegal
    if (page < 0 || posts_per_page * page > res.size ())
      {
	cout << "<div class=\"heading\">" << h1 (str_illegal_page_number) << "</div>" << endl << searchform << endl;;
	cout << endl << site_counter << endl << "</body></html>" << endl;
	con.close ();
	return 0;
      }

    // display heading
    cout << "<div class=\"heading\">" << h1 ((str_page_heading + " '" + searchkeyword + "'").c_str ()) << "</div>" << endl << searchform << endl;

    bool hasInArticle;
    bool hasInComment;
    bool toDisplay;
    bool hasResult;
    hasResult = false;

    if (res)
      {
	Row row, row1;
	Row::size_type i, j, k, l;
	match_results < string::iterator > what, what1;
	string::iterator end, end1, start, start1;
	string strart, str, str1, str2, str3, strtemp1, strtemp2;
	regex e1, e2;
	e1.assign (string ("(") + searchkeyword_regex + string (")"), regex::perl | case_sensitive ? 0 : regex::icase);
	string reg;
	string format, format1;

        // regex for replacing html tags
	reg = "([=\\-_—]{5,})";
	format = "(?1&nbsp;&nbsp;)";

	reg += "|([~]{5,})";
	format += "(?2&nbsp;&nbsp;)";

	reg += "|(<a .*?>)";
	format += "(?3)";

	reg += "|(</a>)";
	format += "(?4)";

	reg += "|(<img .*?>)";
	format += "(?5)";

	reg += "|(<embed .*?>)";
	format += "(?6)";

	reg += "|(<del .*?>)";
	format += "(?7)";

	reg += "|(<ins .*?>)";
	format += "(?8)";

	reg += "|(</ins>)";
	format += "()";

	reg += "|(<br>)";
	format += "()";

	reg += "|(<br />)";
	format += "()";

	reg += "|(\\[tex\\])";
	format += "()";

	reg += "|(\\[/tex\\])";
	format += "()";

	reg += "|(<blockquote>)";
	format += "()";

	reg += "|(</blockquote>)";
	format += "()";

	reg += "|(<b>)";
	format += "()";

	reg += "|(</b>)";
	format += "()";

	reg += "|(<strong>)";
	format += "()";

	reg += "|(</strong>)";
	format += "()";

	reg += "|(<ul>)";
	format += "()";

	reg += "|(</ul>)";
	format += "()";

	reg += "|(<li>)";
	format += "()";

	reg += "|(</li>)";
	format += "()";

	reg += "|(<em>)";
	format += "()";

	reg += "|(</em>)";
	format += "()";

	reg += "|(<ol>)";
	format += "()";

	reg += "|(</ol>)";
	format += "()";

	reg += "|(<code>)";
	format += "()";

	reg += "|(</code>)";
	format += "()";

	reg += "|(<strike>)";
	format += "()";

	reg += "|(</strike>)";
	format += "()";

	reg += "|(</embed>)";
	format += "()";

	reg += "|(<table.*?>)";
	format += "()";

	reg += "|(</table>)";
	format += "()";

	reg += "|(<tr.*?>)";
	format += "()";

	reg += "|(</tr>)";
	format += "()";

	reg += "|(<td.*?>)";
	format += "()";

	reg += "|(</td>)";
	format += "()";

	reg += "|(<font.*?>)";
	format += "()";

	reg += "|(</font>)";
	format += "()";
        // end: regex for replacing html tags
        
        // regex for highlighting
	e2.assign (reg, regex::perl | regex::icase);
	format1 = string ("(<font style=\"background-color\\:yellow\">") + string ("$&</font>)");
        // end: regex for highlighting

	i = 0;
	l = 0;
	while (i < res.size () && l < posts_per_page * (page + 1))
	  {
	    if (l < posts_per_page * page)
	      toDisplay = false;
	    else
	      toDisplay = true;
	    hasInArticle = false;
	    hasInComment = false;
	    row = res.at (i);
	    strtemp1 = row["post_content"].c_str ();

	    strart = string ("<div class=\"title\"><b><a target=\"_new\" href=\"") + string (row["guid"].c_str ()) + "\">" + row["post_title"].c_str () + "</a></b><small><font style=\"color:gray\"> " + row["post_date"].c_str () + "</font></small></div>\n";
	    str2 = row["post_title"].c_str ();

	    if (regex_search (strtemp1.begin (), strtemp1.end (), e1, match_default | format_all) || regex_search (str2.begin (), str2.end (), e1, match_default | format_all)) 
            // regex matching before replacing HTML tags for sake of speed, since most posts wouldn't satisfy
	      {

		str = regex_replace (strtemp1, e2, format, match_default | format_all); // replace HTML tags
		if (regex_search (str.begin (), str.end (), what, e1, match_default | format_all) || regex_search (str2.begin (), str2.end (), e1, match_default | format_all))
                //search for keyword in both post title and content
		  {
		    if (!toDisplay)
		      {
			i = i + 1;
			l = l + 1;
			continue;
		      }
		    hasInArticle = true;
		    cout << strart; // display post title

                    // begin UTF8 trimming for post content
		    start = what[0].first > post_before_hl_char + str.begin ()? what[0].first - post_before_hl_char : str.begin ();
		    while (!((unsigned char) (*start) < 128 || (unsigned char) (*start) >= 192))
		      {
			start++;
		      };
		    end = what[0].first + post_after_hl_char > start + max_post_char ? what[0].first + post_after_hl_char : start + max_post_char;
		    while (!((unsigned char) (*end) < 128 || (unsigned char) (*end) >= 192))
		      {
			end--;
		      };
                    // end UTF8 trimming for post content

		    cout << "<div class=\"content\">";
		    if (str.begin () < start)
		      cout << "<b>...</b> &nbsp;</a>";

		    strtemp2 = regex_replace (string (start, str.end () > end ? end : str.end ()), e1, format1, match_default | format_all); //highlighting
		    cout << strtemp2 << endl;
		    if (str.end () > end || str.begin () < start)
		      cout << "&nbsp;<b>...</b> ";
		    cout << "</div>\n";
		  }
	      }

	    if (!ignore_comments)
	      {
		res1 = query.store ((string ("select * from ") + mysql_comments_table + " where comment_post_ID=" + string (row["ID"])+" order by comment_date").c_str ()); // select comment table if don't ignore comments
		if (res1)
		  {
		    for (j = 0, k = 0; row1 = res1.at (j); ++j)
		      {
			if (k == max_comments_per_post) // number of comments 
                                                        //should not exceed max_comments_per_post
			  {
			    cout << "<div class=\"comment\"><a target=\"_new\" href=\"" << row["guid"] << "?cp=all#comments\"><b>" + str_more_comments_in_results + "</b></a></div>" << endl;
			    break;
			  }
			strtemp1 = row1["comment_content"].c_str ();
			str3 = row1["comment_author"].c_str ();

			if (regex_search (strtemp1.begin (), strtemp1.end (), e1, match_default | format_all) || regex_search (str3.begin (), str3.end (), e1, match_default | format_all))
            // regex matching before replacing HTML tags for sake of speed, since most comments wouldn't satisfy
			  {
			    str1 = regex_replace (strtemp1, e2, format, match_default | format_all);// replace HTML tags 
			    if (regex_search (str1.begin (), str1.end (), what1, e1, match_default | format_all) || regex_search (str3.begin (), str3.end (), e1, match_default | format_all))
                            //search results in both comment author and content
			      {
				if (!toDisplay)
				  {
				    hasInComment = true;
				    break;
				  }

				if (!hasInComment && !hasInArticle) // if post is in paging range, output
				  {
				    hasInComment = true;
				    cout << strart; 
				  }

                                // begin UTF8 trimming for comment content
				start1 = what1[0].first > comment_before_hl_char + str1.begin ()? what1[0].first - comment_before_hl_char : str1.begin ();
				while (!((unsigned char) (*start1) < 128 || (unsigned char) (*start1) >= 192))
				  {
				    start1++;
				  }
				end1 = what1[0].first + comment_after_hl_char > start1 + max_comment_char ? what1[0].first + comment_after_hl_char : start1 + max_comment_char;
				while (!((unsigned char) (*end1) < 128 || (unsigned char) (*end1) >= 192))
				  {
				    end1--;
				  };
                                // end UTF8 trimming for comment content

				k++;

				cout << "<div class=\"comment\"><font style=\"color:green\">";

				cout << "<a target=\"_new\" href=\"" << string (row["guid"].c_str ()) << "?cp=all#comment-" << row1["comment_ID"] << "\">";
				regex_replace (out, str3.begin (), str3.size () > max_comment_char ? str3.begin () + max_comment_char : str3.end (), e1, format1, match_default | format_all);// highlighting comment author
				cout << "</a></font>&nbsp;";
				if (str1.begin () < start1)
				  cout << "<b>...</b>&nbsp;</a>"; 

				strtemp2 = regex_replace (string (start1, str1.end () > end1 ? end1 : str1.end ()), e1, format1, match_default | format_all); //highlighting comment content
				cout << strtemp2;
				if (str1.end () > end1 || str1.begin () < start1)
				  cout << "&nbsp;<b>...</b>";

				cout << "</div>" << endl;
			      }
			  }
		      }
		  }
	      }

	    i++;
	    if (hasInArticle || hasInComment)
	      {
		l++;
		hasResult = true;
	      }
	  }

	if (!hasResult) // handle if no result is found
	  {
	    cout << "<div class=\"title\"><b>" + str_no_result_found + "</b></div>" << endl;
	  }

	cout << "<div class=\"footer\">";

        // begin paging
	if (page != 0)
	  {
	    cout << "<a href=\"wpspp.cgi?s=" << searchkeyword;
	    if (page != 1)
	      cout << "&p=" << page - 1;
	    if (case_sensitive)
	      cout << "&cs=1";
	    if (ignore_comments)
	      cout << "&ic=1";
	    if (use_or_logic)
	      cout << "&uol=1";
	    if (posts_per_page != 10)
	      cout << "&ppp=" << posts_per_page;
	    cout << "\"><b>&laquo; " << str_previous_page << "</b></a>";
	  }
	if (page && i < res.size ())
	  cout << "&nbsp; |&nbsp; ";
	if (i < res.size ())
	  {
	    cout << "<a href=\"wpspp.cgi?s=" << searchkeyword << "&p=" << page + 1;
	    if (case_sensitive)
	      cout << "&cs=1";
	    if (ignore_comments)
	      cout << "&ic=1";
	    if (use_or_logic)
	      cout << "&uol=1";
	    if (posts_per_page != 10)
	      cout << "&ppp=" << posts_per_page;
	    cout << "\"><b>" << str_next_page << " &raquo;</b></a>";
	  }
	if ((page || i < res.size ()) && hasResult)
	  {
	    cout << "<br>";
	  }
        // end paging

        // display searching duration
	endtime = posix_time::microsec_clock::local_time ();
	duration = endtime - starttime;
	cout << "<small><font style=\"color:gray\">";
	printf ("%.3lf", duration.total_milliseconds () / 1000.0);
	cout << " " << str_seconds << "</font></small>";

	cout << "</div>" << endl;

      }
    con.close (); // close mysql connection
    cout << endl << site_counter << endl << "</body></html>" << endl; //closing body and html tags
  }
  catch (exception & e)
  {
    cout << str_unknown_error << endl; // "handle" unknown error!
  }
  return 0;			// To avoid Apache errors.
}
